﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NHibernate;
using Pink.Scheduler.DataAccess;
using Pink.Scheduler.RemoteInterfaces.Enum;
using NHibernate.Criterion;

namespace Pink.Scheduler.RemoteInterfaces
{
	/// <summary>
	/// Класс, отвечающий за управление состояниями выполнения
	/// </summary>
	public class ExecutionStateManager
	{
		ISessionFactory factory;
		/// <summary>
		/// Конструктор
		/// </summary>
		/// <param name="factory">Фабрика сессий</param>
		public ExecutionStateManager(ISessionFactory factory)
		{
			this.factory = factory;
		}

		/// <summary>
		/// Занести в БД информацию о том, что мы начали выполнять задачу по расписанию
		/// </summary>
		/// <param name="task">Какую задачу мы начали выполнять</param>
		/// <param name="alreadyRunning">Если задача уже запущена или в очереди, то запуски по
		/// расписанию запрещены и возвращается null</param>
		/// <returns></returns>
		public TaskExecution StartNew(TaskData task, out TaskExecution alreadyRunning)
		{
			using (ISession session = factory.OpenSession())
			{
				// привяжем задачу к этой сессии
				TaskData thisSessionTask = session.Load<TaskData>(task.ID);
				//session.Transaction.Begin();
				// найдём уже запущенную
				alreadyRunning = session.CreateCriteria<TaskExecution>()
					.Add(Expression.Eq(TaskExecution.TaskProp, thisSessionTask))
					.Add(Expression.Le(TaskExecution.StartTimeProp, DateTime.Now))
					.Add(Expression.In(TaskExecution.StateProp,
						new ExecutionState[] { ExecutionState.Created, ExecutionState.Running }))
					.AddOrder(Order.Desc(TaskExecution.UpdatedAtProp))
					.SetMaxResults(1)
					.UniqueResult<TaskExecution>();
				// повторные запуски по расписанию запрещены
				if (alreadyRunning != null)
					return null;
				TaskExecution execution = new TaskExecution();
				execution.Task = thisSessionTask;
				execution.StartTime = DateTime.Now;
				return UpdateStateAndEvict(session, execution, ExecutionState.Running);
			}
		}

		/// <summary>
		/// Занести в БД информацию о том, что мы начали выполнять задачу из 
		/// очереди ручных запусков
		/// </summary>
		/// <param name="task">Задача, для которую мы хотим выполнять</param>
		/// <returns></returns>
		public TaskExecution Dequeue(TaskData task)
		{
			using (ISession session = factory.OpenSession())
			{
				// привяжем задачу к этой сессии
				TaskData thisSessionTask = session.Get<TaskData>(task.ID);
				// найдём первую поставленную в очереди на выполнение.
				// транзакция, чтобы не схватить эту задачу параллельно с кем-нибудь ещё
				//session.BeginTransaction(System.Data.IsolationLevel.Serializable);
				TaskExecution queued = session.CreateCriteria<TaskExecution>()
					.Add(Expression.Eq(TaskExecution.TaskProp, thisSessionTask))
					.Add(Expression.Eq(TaskExecution.StateProp, ExecutionState.Created))
					.AddOrder(Order.Asc(TaskExecution.StartTimeProp))
					.SetMaxResults(1)
					.UniqueResult<TaskExecution>();
				if (queued == null)
					return null;
				return UpdateStateAndEvict(session, queued, ExecutionState.Running);
			}
		}

		/// <summary>
		/// Проверить, стоит ли повторять неудавшуюся задачу и обновить её соответствующим образом
		/// </summary>
		/// <param name="execution">Задача, которую проверяем</param>
		/// <param name="retryIn">Когда повторить, -1 миллисекунда, если никогда</param>
		/// <returns>Обновлённое состояние задачи: если status = failed, 
		/// то повторять не надо, если running, то надо</returns>
		public TaskExecution Retry(TaskExecution execution, out TimeSpan retryIn)
		{
			lock (execution)
			{
				using (ISession session = factory.OpenSession())
				{
					//session.BeginTransaction();
					TaskExecution updated = session.Get<TaskExecution>(execution.ID);
					// периодические задачи не повторяются, просроченные тоже
					if (updated.Task.ScheduleType == ScheduleType.Periodic ||
						!updated.Task.RetryWithinSeconds.HasValue ||
						updated.StartTime + TimeSpan.FromSeconds(updated.Task.RetryWithinSeconds.Value) < DateTime.Now)
					{
						retryIn = TimeSpan.FromMilliseconds(-1);
						return UpdateStateAndEvict(session, updated, ExecutionState.Failed);
					}
					else
					{
						// если нас просили повторить, но задержку не задали -- она 0
						retryIn = updated.Task.RetryDelaySeconds.HasValue ?
							TimeSpan.FromSeconds(updated.Task.RetryDelaySeconds.Value) :
							TimeSpan.FromSeconds(0);
						updated.RetryCount += 1;
						session.Update(execution);
						session.Flush();
						session.Evict(execution);
						return updated;
					}
				}
			}
		}


		/// <summary>
		/// Служебный метод, отмечающий изменение состояния
		/// </summary>
		/// <param name="session"></param>
		/// <param name="execution"></param>
		/// <param name="state"></param>
		/// <returns></returns>
		private TaskExecution UpdateStateAndEvict(ISession session, 
			TaskExecution execution, ExecutionState state)
		{
			execution.State = state;
			execution.UpdatedAt = DateTime.Now;
			session.SaveOrUpdate(execution);
			//session.Transaction.Commit();
			session.Flush();
			session.Evict(execution);
			return execution;
		}
		/// <summary>
		/// Занести в БД информацию о том, что мы закончили выполнение задачи.
		/// </summary>
		/// <param name="execution">Какое выполнение мы закончили</param>
		/// <param name="completionState">В каком состоянии закончили выполнение</param>
		public void CompleteRunningTask(TaskExecution execution, ExecutionState completionState)
		{
			lock (execution)
			{
				// сохраним состояние задачи
				using (ISession session = factory.OpenSession())
				{
					//session.BeginTransaction(System.Data.IsolationLevel.Serializable);
					// привяжем объект к новой сессии
					TaskExecution updated = session.Get<TaskExecution>(execution.ID);
					updated.UpdatedAt = DateTime.Now;
					updated.State = completionState;
					session.Update(updated);
					session.Flush();
					//session.Transaction.Commit();
				}
			}
		}
	}
}
